Skip to content

Airitme bundle purchase#71

Open
abubakar508 wants to merge 2 commits intonutcas3:mainfrom
abubakar508:airitme-bundle-purchase
Open

Airitme bundle purchase#71
abubakar508 wants to merge 2 commits intonutcas3:mainfrom
abubakar508:airitme-bundle-purchase

Conversation

@abubakar508
Copy link
Copy Markdown

feat(charging/types): add Bundle, ActiveBundle, and AirtimeBalance types

Adds all data models required for airtime and bundle distribution:

  • BundleType enum (Data, Voice, SMS, Hybrid) with as_str()/from_str()
  • BundleAllowances (data_bytes, voice_seconds, sms_count, roaming_data_bytes)
  • Bundle (bundle_id, name, type, allowances, validity_days, priority,
    amount_unconverted, is_active)
  • ActiveBundle (imsi, bundle_id, priority, activated_at, expires_at,
    remaining_allowances) with Redis FromRedisValue/ToRedisArgs impls
  • AirtimeBalance (imsi, home_seconds, roaming_seconds, last_updated)
  • AirtimeDeduction (deducted, new_balance)

fix(charging): make RatingPlansRepo pool pub(crate) for bundle service access

fix(errors): add BundleNotFound, BundleAlreadyActive, InvalidBundleConfig
variants with correct HTTP status mappings (404, 409, 400)

feat(charging): implement airtime service

Adds airtime management on ChargingEngine via airtime_service.rs:

  • add_airtime(imsi, seconds, roaming) — Lua atomic INCRBY on
    Redis airtime:home:{imsi} or airtime:roaming:{imsi}
  • get_airtime_balance(imsi) → AirtimeBalance
  • deduct_airtime(imsi, seconds, roaming) — Lua atomic check-and-decrby;
    returns InsufficientAirtime if balance < requested
  • All operations protected by redis_circuit_breaker

feat(charging): implement bundle service with airtime-funded purchase

Adds bundle lifecycle management on ChargingEngine via bundle_service.rs:

  • create_bundle / list_bundles / get_bundle_config / get_bundle_by_id
  • activate_bundle_for_subscriber — stores ActiveBundle in Redis with TTL,
    maintains bundle:active:set:{imsi} Redis Set for O(1) lookup,
    writes subscriber_bundles row to PostgreSQL
  • consume_from_bundle — priority-ordered deduction from active bundles
  • deactivate_bundle — marks bundle inactive in PostgreSQL
  • purchase_bundle_with_airtime(imsi, bundle_id) — atomically deducts
    amount_unconverted from airtime balance then activates bundle
  • gift_bundle(sender_msisdn, recipient_msisdn, bundle_id) — resolves
    both MSISDNs to IMSIs, deducts from sender airtime, activates on
    recipient
  • expire_stale_bundles — background cleanup of expired subscriber_bundles
  • record_bundle_activation — Redis audit trail per activation
  • All PostgreSQL ops protected by postgres_circuit_breaker

feat(charging): register airtime_service and bundle_service modules

Adds pub mod declarations to apps/charging-engine/src/charging.rs so
rust-analyzer and the compiler can resolve the new files.

feat(handlers): add airtime and bundle HTTP handlers

handlers/airtime.rs:

  • POST /v1/airtime/:imsi/add → add_airtime
  • GET /v1/airtime/:imsi/balance → get_airtime_balance
  • POST /v1/airtime/:imsi/deduct → deduct_airtime

handlers/bundles.rs:

  • POST /v1/bundles → create_bundle
  • GET /v1/bundles → list_bundles
  • GET /v1/bundles/:id → get_bundle
  • DELETE /v1/bundles/:id → deactivate_bundle
  • POST /v1/bundles/:id/activate → activate_bundle
  • GET /v1/bundles/:imsi/active → list_active_bundles
  • POST /v1/bundles/:id/purchase → purchase_bundle (by MSISDN, deducts airtime)
  • POST /v1/bundles/:id/gift → gift_bundle (sender_msisdn → recipient_msisdn)

Updates handlers/mod.rs with pub mod and pub use for both new modules.

feat(routes): register airtime and bundle routes in protected section

All new routes sit inside the auth_middleware + GovernorLayer stack,
inheriting rate limiting and JWT authentication automatically.

feat(migrations): add bundles, subscriber_bundles, airtime tables

00019_init_bundles.sql

  • bundles table: bundle_id, name, bundle_type, individual allowance
    columns (data_bytes, voice_seconds, sms_count, roaming_data_bytes),
    validity_days, priority, amount_unconverted, is_active

00020_init_subscriber_bundles.sql

  • subscriber_bundles table: FK to subscribers.imsi + FK to
    bundles.bundle_id, activated_at, expires_at, remaining allowance
    columns for deduction tracking

00021_init_airtime_balances.sql

  • airtime_balances table: imsi, home_seconds, roaming_seconds,
    last_updated — durable source of truth synced from Redis

00022_init_airtime_transactions.sql

  • airtime_transactions table: imsi, transaction_type (top_up/deduction/
    bundle_purchase/gift_sent/gift_received), seconds_delta, bundle_id,
    created_at — full audit trail

00023_alter_usage_records_charging_source.sql

  • ALTER usage_records: adds charging_source TEXT ('bundle'|'credit')
    and subscriber_bundle_id INTEGER for billing reconciliation

Added airtime table migraiton

OPc must be computed as AES-128(K, OP) XOR OP, not just AES-128(K, OP).

The missing XOR step produced incorrect OPc values incompatible with any

standard Milenage implementation.

- Add XOR of encrypted block with OP in generateOPc()
- Add Milenage f1–f5 helper functions (runMilenage, xorBytes,
  rotateLeft)

feat(subscriber): add SQN field for 3GPP AKA replay protection

Sequence number (SQN) is required by the Milenage AKA protocol to
prevent replay attacks. It is incremented atomically on each auth
vector generation.

feat(handlers): expose AuC auth-vector endpoint

Adds POST /api/v1/auc/:imsi/auth-vector for HSS/MME to request
authentication vectors during subscriber attach procedures.

- AuCHandler struct with SubscriberService dependency
- Returns { rand, xres, ck, ik, autn } as hex strings
- Returns 404 if IMSI not found, 500 on crypto/DB failure
- Register route in router

feat(auc): implement Authentication Center with Milenage AKA

Adds GenerateAuthVector() which produces a full 3GPP AKA authentication

vector (RAND, XRES, CK, IK, AUTN) for a given IMSI using the Milenage
algorithm (3GPP TS 35.205/206).

- Load subscriber K and OPc from DB by IMSI
- Atomically increment SQN via UPDATE … RETURNING (no race condition)
- Generate 16-byte RAND via crypto/rand
- Run Milenage f1–f5 to derive XRES, CK, IK, AK, MAC-A
- Construct AUTN = (SQN XOR AK) || AMF || MAC-A

Files changed:
M apps/api-server/internal/services/subscriber_auth.go
A apps/api-server/internal/services/subscriber_auc.go
A apps/api-server/internal/handlers/auc_handler.go
M apps/api-server/internal/models/subscriber.go
A migrations/00019_add_subscriber_sqn.sql

  add: new migration for teh sequencing number attached to the
  Authentication Center (AuC)

- Add SQN int64 field to models.Subscriber (gorm:"default:0")
- Add migration 00019_add_subscriber_sqn.sql:
  ALTER TABLE subscribers ADD COLUMN sqn BIGINT NOT NULL DEFAULT 0
access

fix(errors): add BundleNotFound, BundleAlreadyActive,
InvalidBundleConfig
variants with correct HTTP status mappings (404, 409, 400)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant